﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using Hockey.Shared;

namespace Hockey.GameEngine
{
    /// <summary>
    /// Game board implementation
    /// </summary>
    public class Board: IBoardForEngine
    {
        /// <summary>
        /// Size of board
        /// </summary>
        private int _width;
        private int _height;

        /// <summary>
        /// Puck direction
        /// </summary>
        private ePuckDir _puckDirection;

        /// <summary>
        /// The node the puck is on
        /// </summary>
        private Node _puckNode;

        /// <summary>
        /// Board situation
        /// </summary>
        private Node[,] _board;

        /// <summary>
        /// Move step stack used by engine for searching the move
        /// </summary>
        private MoveStepStack _stepsStack;

        
        /// <summary>
        /// Game History Record
        /// </summary>
        private Stack<MovePath> _movesDone;
        private Stack<MovePath> _movesToDo;

        /// <summary>
        /// Constructor
        /// </summary>
        public Board()
        {
            _stepsStack = new MoveStepStack();
            _movesDone = new Stack<MovePath>();
            _movesToDo = new Stack<MovePath>();
        }

        #region Property Wrappers
        
        public int Width
        {
            get { return _width; }
        }

        public int Height
        {
            get { return _height; }
        }

        public ePuckDir PuckDirection
        {
            get { return _puckDirection; }
        }

        public INode PuckNode
        {
            get
            {
                return _puckNode;
            }
        }


        #endregion

        #region Public Interface Implementation
        

        /// <summary>
        /// Initializes game board
        /// </summary>
        public void Init(int width, int height, ePuckDir puckDirection = ePuckDir.Up)
        {
            // check valid size
            if (width.IsOdd() == false)
                throw new ApplicationException("Width must be odd.");
            if (height.IsOdd() == false)
                throw new ApplicationException("Height must be odd.");

            // set attributes
            _width = width;
            _height = height + 2; // add height for two gate fields
            _puckDirection = puckDirection;

            // allocate board
            _board = new Node[_height, _width];

            // create the board graph network of nodes and bonds
            AllocateBoardGraph();            
        }

        /// <summary>
        ///  Returns the node
        /// </summary>
        public INode Get(Coordinate coor)
        {
            return Get(coor.Row, coor.Col);
        }

        /// <summary>
        ///  Returns the node
        /// </summary>
        public INode Get(int row, int col)
        {
            // do coordinate check
            if (row < 0 || col < 0 || row >= _height || col >= _width)
                throw new ArgumentOutOfRangeException("Coordinate");

            return _board[row, col];
        }

        /// <summary>
        /// Returns true when the move is not in progress
        /// </summary>
        public bool IsMoveCompleted
        {
            get { return _stepsStack.Count == 0; }
        }

        /// <summary>
        /// Starts the new game
        /// </summary>
        public void NewGame()
        {
            // create the new network
            AllocateBoardGraph();

            // set puck direction
            _puckDirection = ePuckDir.Up;

            // clear out stacks
            _stepsStack.Clear();
            _movesDone.Clear();
            _movesToDo.Clear();
            
        }

        /// <summary>
        /// Loads the game from stream
        /// </summary>
        public void Load(Stream stream)
        {
            // set empty board
            NewGame();

            // deserialize movelist
            List<MovePath> moveList = null;
            BinaryFormatter formatter = new BinaryFormatter();
            moveList = formatter.Deserialize(stream) as List<MovePath>;
            if (moveList == null)
                throw new ApplicationException("Invalid format.");

            // do the moves
            foreach (MovePath move in moveList)
            {
                foreach (var direction in move)
                {
                    bool isFinal;
                    Play(direction, out isFinal);
                }
            }
        }

        /// <summary>
        /// Saves the game to the stream
        /// </summary>
        public void Save(Stream stream)
        {
            // get the moves
            List<MovePath> moveList = new List<MovePath>();
            moveList.AddRange(_movesDone.Reverse());
            moveList.AddRange(_movesToDo);

            // save them
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, moveList);                      
        }


        public void Play(eMoveDirection direction, out  bool isFinalMove)
        {
            // play the move
            PlayMove(direction, out isFinalMove);

            // push to the stack
            _stepsStack.Push(direction, isFinalMove);

            // clear the stack of moves to do
            _movesToDo.Clear();

            // check the final move step
            if (isFinalMove || HasGameFinished())
            {
                // add complete move path to the game history
                MovePath path = new MovePath(isFinalMove);
                path.AddRange(_stepsStack.Reverse());
                _movesDone.Push(path);
                _stepsStack.Clear();
            }
        }

        /// <summary>
        /// Checks whether player can move in this direction
        /// Additionally checks the end of the game
        /// </summary>
        public bool CanPlayInThisDirection(eMoveDirection direction)
        {
            // first check the end of game
            if (HasGameFinished())
                return false;

            // then check the direction
            return IsAllowedToMoveInDirection(direction);
        }

        /// <summary>
        /// Determines whether there is end of game
        /// Winner side is determined by the puck direction
        /// </summary>
        /// <returns></returns>
        public bool HasGameFinished()
        {
            // first check, if we the puck has reached the gate
            if (_puckNode.IsGate)
                return true;

            // then check, if we have legal moves
            foreach (var pair in _puckNode.Bonds)
            {
                // if there is a way to move in any direction, there is no game end
                if (IsAllowedToMoveInDirection(pair.Key) == true)
                    return false;
            }

            // there is no way to move in any direction
            return true;
        }

        /// <summary>
        /// Returns the winning side
        /// </summary>
        public ePuckDir? GetWinner()
        {
            // not end of the game, no result
            if (HasGameFinished() == false)
                return null;

            // if gate, resolve the winner
            if (_puckNode.IsGate)
            {
                if (_puckNode.Coordinate.Row == 0)
                    return ePuckDir.Up;
                else
                    return ePuckDir.Down;
            }

            // else there is no move, winning is the opposite side, which did not get to the jam
            return PuckDirection == ePuckDir.Up ? ePuckDir.Down : ePuckDir.Up;
        }


        /// <summary>
        /// Returns whether we can navigate back
        /// </summary>
        public bool CanMoveBack()
        {
            return _movesDone.Count != 0 || _stepsStack.Count != 0;
        }

        /// <summary>
        /// Returns whether we can navigate the game forward
        /// </summary>
        public bool CanMoveForward()
        {
            return _movesToDo.Count != 0 && _stepsStack.Count == 0;
        }

        /// <summary>
        /// Returns one move back
        /// </summary>
        public void MoveBack()
        {
            // init vars
            bool isFinal = false;
            List<eMoveDirection> path = new List<eMoveDirection>();

            // get the list of moves to undo
            if (_stepsStack.Count != 0)
            {
                path.AddRange(_stepsStack.Reverse());
                _stepsStack.Clear();
            }
            else
            {
                if (_movesDone.Count == 0)
                    throw new ApplicationException("Cannot move back.");

                // move back
                MovePath movePath = _movesDone.Pop();
                _movesToDo.Push(movePath);

                // convert model
                isFinal = movePath.IsFinal;
                path.AddRange(movePath);                        
            }

            // undo all move steps
            for (int i = path.Count - 1; i >= 0; i--)
            {
                PlayMoveBack(path[i], isFinal);
                isFinal = false;
            }          
        }

        /// <summary>
        /// Navigates one move forward in the game history
        /// </summary>
        public void MoveForward()
        {
            // check the stack
            if (_movesToDo.Count == 0)
                throw new ApplicationException("Cannot move forward.");

            // navigate in the game history
            MovePath path = _movesToDo.Pop();
            _movesDone.Push(path);

            // dummy var
            bool isFinal;

            // do the move steps
            for (int i = 0; i < path.Count; i++)
            {
                PlayMove(path[i], out isFinal);
            }
        }

        /// <summary>
        /// Move to the beginning of the game
        /// </summary>
        public void MoveToBeginning()
        {
            while (CanMoveBack())
                MoveBack();
        }

        /// <summary>
        /// Move to the end of the game
        /// </summary>
        public void MoveToEnd()
        {
            while (CanMoveForward())
                MoveForward();
        }

        #endregion

        #region Engine Interface Methods

        /// <summary>
        /// Engine playing method
        /// </summary>
        public void EnginePlay(eMoveDirection direction, out bool isFinalMove)
        {
            // play the move
            PlayMove(direction, out isFinalMove);

            // push it to the stack
            _stepsStack.Push(direction, isFinalMove);
        }

        /// <summary>
        /// Move-returning method for engine
        /// </summary>
        public void EnginePlayBack()
        {
            // get the last move
            bool isFinal = _stepsStack.IsFinal;
            eMoveDirection direction = _stepsStack.Pop();

            // play back
            PlayMoveBack(direction, isFinal);
        }

        #endregion

        #region Private methods

        /// <summary>
        /// Checks whether player can move in this direction
        /// </summary>
        private bool IsAllowedToMoveInDirection(eMoveDirection direction)
        {
            // check the puck
            Debug.Assert(_puckNode != null);
            if (_puckNode == null)
                throw new ArgumentNullException("puckNode");

            // if bond does not exist, return false
            IBond iBond = null;
            if (_puckNode.Bonds.TryGetValue(direction, out iBond) == false)
                return false;

            // check, if bond has not been visited
            if (iBond.HasBeenVisited == true)
                return false;

            return true;
        }

        /// <summary>
        /// Common method for playing move
        /// </summary>
        private void PlayMove(eMoveDirection direction, out bool isFinalMove)
        {
            // check the direction
            if (CanPlayInThisDirection(direction) == false)
                throw new ApplicationException("Wrong direction to play.");

            // if bond does not exist, return false
            IBond iBond = null;
            if (_puckNode.Bonds.TryGetValue(direction, out iBond) == false)
                throw new ApplicationException("Bond not found.");

            // get destination node 
            INode dstNode = iBond.GetNextNode(_puckNode);
            Debug.Assert(dstNode != null);

            // resolve if final move step
            isFinalMove = false;
            if (dstNode.HasBeenVisited == false)
                isFinalMove = true;

            // place puck
            (iBond as Bond).MarkAsVisited(_puckDirection);
            PlacePuck(dstNode.Coordinate.Row, dstNode.Coordinate.Col);

            // if final move, change puck orientation
            // allow only when there is not a game end
            if (isFinalMove && HasGameFinished() == false)
            {
                ChangePuckDirection();
            }
        }

        /// <summary>
        /// Common method for playing move back
        /// </summary>
        private void PlayMoveBack(eMoveDirection direction, bool isFinal)
        {
            // if final move, unmark the node as visited
            if (isFinal || HasGameFinished())
            {
                // change only in case there is not a game end
                if (HasGameFinished() == false)
                {
                    ChangePuckDirection();
                }
                
                if(isFinal)
                    _puckNode.UnmarkAsVisited();
            }

            // remove the puck from the current node
            _puckNode.RemovePuck();

            // get the bond and mark it as unvisited
            Bond bond = _puckNode.Bonds[direction.Opposite()] as Bond;
            bond.MarkAsUnvisited();

            // find previous node
            _puckNode = bond.GetNextNode(_puckNode) as Node;

            // place puck
            _puckNode.GainPuck();
        }



        /// <summary>
        /// Changes side to move
        /// </summary>
        private void ChangePuckDirection()
        {
            _puckDirection = _puckDirection == ePuckDir.Up ? ePuckDir.Down : ePuckDir.Up;
        }

        /// <summary>
        /// Creates all nodes and bonds among them
        /// </summary>
        private void AllocateBoardGraph()
        {
            int middleRow = _height / 2;
            int middleCol = _width / 2;

            // create the main node grid
            for (int row = 1; row < _height - 1; row++)
            {
                for (int col = 0; col < _width; col++)
                {
                    Node node = new Node(new Coordinate(row, col));
                    _board[row, col] = node;

                    // mark border node as visited
                    if (IsBorderNode(node))
                        node.MarkAsVisited();
                }
            }

            // create upper and lower node-gate level
            for (int col = 0; col < _width; col++)
            {
                _board[0, col] = null;
                _board[_height - 1, col] = null;
            }

            // create gates
            _board[0, middleCol] = new Node(new Coordinate(0, middleCol));
            _board[0, middleCol].MarkAsGate();
            _board[_height - 1, middleCol] = new Node(new Coordinate(_height - 1, middleCol));
            _board[_height - 1, middleCol].MarkAsGate();


            // create the main node grid
            for (int row = 1; row < _height - 1; row++)
            {
                for (int col = 0; col < _width; col++)
                {
                    Coordinate coor = new Coordinate(row, col);
                    Node curNode = _board[row, col];

                    if (col < _width - 1)
                    {
                        BondTwoNodes(curNode, Get(GetCoordinatesInDirection(coor, eMoveDirection.East)) as Node, eMoveDirection.East);                        
                    }
                    if (row < _height - 2)
                    {
                        BondTwoNodes(curNode, Get(GetCoordinatesInDirection(coor, eMoveDirection.South)) as Node, eMoveDirection.South);                        

                        if(col > 0)
                            BondTwoNodes(curNode, Get(GetCoordinatesInDirection(coor, eMoveDirection.SouthWest)) as Node, eMoveDirection.SouthWest);                        
                        if (col < _width - 1)
                            BondTwoNodes(curNode, Get(GetCoordinatesInDirection(coor, eMoveDirection.SouthEast)) as Node, eMoveDirection.SouthEast);                                         
                    }
                }
            }

            // gate node bonding

            // upper gate
            Coordinate gateCoor = new Coordinate(0, middleCol);
            Node gateNode = _board[gateCoor.Row, gateCoor.Col];
            BondTwoNodes(gateNode, Get(GetCoordinatesInDirection(gateCoor, eMoveDirection.SouthEast)) as Node, eMoveDirection.SouthEast);
            BondTwoNodes(gateNode, Get(GetCoordinatesInDirection(gateCoor, eMoveDirection.South)) as Node, eMoveDirection.South);
            BondTwoNodes(gateNode, Get(GetCoordinatesInDirection(gateCoor, eMoveDirection.SouthWest)) as Node, eMoveDirection.SouthWest);

            // bottom gate
            gateCoor = new Coordinate(_height - 1, middleCol);
            gateNode = _board[gateCoor.Row, gateCoor.Col];
            BondTwoNodes(gateNode, Get(GetCoordinatesInDirection(gateCoor, eMoveDirection.NorthEast)) as Node, eMoveDirection.NorthEast);
            BondTwoNodes(gateNode, Get(GetCoordinatesInDirection(gateCoor, eMoveDirection.North)) as Node, eMoveDirection.North);
            BondTwoNodes(gateNode, Get(GetCoordinatesInDirection(gateCoor, eMoveDirection.NorthWest)) as Node, eMoveDirection.NorthWest);


            // set puck in the middle
            PlacePuck(middleRow, middleCol);
        }

        /// <summary>
        /// Makes the bond between two nodes
        /// </summary>
        private void BondTwoNodes(Node node1, Node node2, eMoveDirection direction)
        {
            // create bond
            Bond bond = new Bond(node1, node2);

            // assign it
            node1.AssignBond(direction, bond);
            node2.AssignBond(direction.Opposite(), bond);

            // if border bond, mark as visited
            if (direction == eMoveDirection.South || direction == eMoveDirection.East)
            {
                if (node1.HasBeenVisited && node2.HasBeenVisited)
                    bond.MarkAsVisited(ePuckDir.Up);
            }
        }

        /// <summary>
        /// Checks, whether node is border node
        /// </summary>
        private bool IsBorderNode(Node node)
        {
            int middleCol = _width / 2;

            int row = node.Coordinate.Row;
            int col = node.Coordinate.Col;

            // if border, mark as visited
            if (col == 0 || col == _width - 1)
                return true;
            if (col != middleCol)
            {
                if (row == 1 || row == _height - 2)
                    return true;
            }

            return false;
        }

        /// <summary>
        /// Calculates the coordinates in the move direction
        /// </summary>
        private Coordinate GetCoordinatesInDirection(Coordinate src, eMoveDirection direction)
        {
            switch (direction)
            {
                case eMoveDirection.North:
                    return new Coordinate(src.Row - 1, src.Col);
                case eMoveDirection.South:
                    return new Coordinate(src.Row + 1, src.Col);
                case eMoveDirection.East:
                    return new Coordinate(src.Row, src.Col + 1);
                case eMoveDirection.West:
                    return new Coordinate(src.Row, src.Col - 1);
                case eMoveDirection.NorthEast:
                    return new Coordinate(src.Row - 1, src.Col + 1);
                case eMoveDirection.NorthWest:
                    return new Coordinate(src.Row - 1, src.Col - 1);
                case eMoveDirection.SouthEast:
                    return new Coordinate(src.Row + 1, src.Col + 1);
                case eMoveDirection.SouthWest:
                    return new Coordinate(src.Row + 1, src.Col - 1);
                default:
                    Debug.Assert(false);
                    throw new ApplicationException("Unknown direction.");
            }
        }

        /// <summary>
        /// Places the puck
        /// </summary>
        private void PlacePuck(int row, int col)
        {
            // remove puck if necessary
            if (_puckNode != null)
            {
                _puckNode.RemovePuck();                
            }

            _puckNode = Get(row, col) as Node;
            
            // place puck
            _puckNode.GainPuck();

            // mark the node as visited
            _puckNode.MarkAsVisited();
        }

        /// <summary>
        /// Container class for move directions
        /// </summary>
        [Serializable]
        private class MovePath : List<eMoveDirection>
        {
            private bool _isFinal;

            public bool IsFinal
            {
                get
                {
                    return _isFinal;
                }
            }

            public MovePath(bool isFinal = true)
            {
                _isFinal = isFinal;
            }
        }
      
        #endregion
    }
}
